基于文件的编程模型中为了提高文件的写入性能,通常会引入内存映射机制,但凡事都有利弊,引入了内存映射、页缓存等机制,数据首先写入到页缓存,此时并没有真正的持久化到磁盘,那 Broker 收到客户端的消息发送请求时是存储到页缓存中就直接返回成功,还是要持久化到磁盘中才返回成功呢?
这里又是一个抉择,是在性能与消息可靠性方面进行的权衡,为此 RocketMQ 提供了多种持久化策略:同步刷盘、异步刷盘。
“刷盘”这个名词是不是听起来很高大上,其实这并不是一个什么神秘高深的词语,所谓的刷盘就是将内存中的数据同步到磁盘,在代码层面其实是调用了 FileChannel 或 MappedBytebuffer 的 force 方法,其截图如下:
接下来分别介绍同步刷盘与异步刷盘的实现技巧。
同步刷盘指的 Broker 端收到消息发送者的消息后,先写入内存,然后同时将内容持久化到磁盘后才向客户端返回消息发送成功。
提出思考:那在 RocketMQ 的同步刷盘是一次消息写入就只将一条消息刷写到磁盘?答案是否定的。
在 RocketMQ 中同步刷盘的入口为 commitlog 的 handleDiskFlush,同步刷盘的截图如下:
这里有两个核心关键点:
接下来继续探讨组提交的设计理念。
判断一条刷盘请求成功的条件:当前已刷盘指针大于该条消息对应的物理偏移量,这里使用了刷盘重试机制。然后唤醒主线程并返回刷盘结果。
所谓的组提交,其核心理念理念是调用刷盘时使用的是 MappedFileQueue.flush 方法,该方法并不是只将一条消息写入磁盘,而是会将当期未刷盘的数据一次性刷写到磁盘,既组提交,故即使在同步刷盘情况下,也并不是每一条消息都会被执行 flush 方法,为了更直观的展现组提交的设计理念,给出如下流程图:
同步刷盘的优点是能保证消息不丢失,即向客户断返回成功就代表这条消息已被持久化到磁盘,即消息非常可靠,但是以牺牲写入性能为前提条件的,但由于 RocketMQ 的消息是先写入 PageCache,故消息丢失的可能性较小,如果能容 忍一定几率的消息丢失,但能提高性能,可以考虑使用异步刷盘。
异步刷盘指的是 Broker 将消息存储到 PageCache 后就立即返回成功,然后开启一个异步线程定时执行 FileChannel 的 forece 方法将内存中的数据定时刷写到磁盘,默认间隔为 500ms。在 RocketMQ 的异步刷盘实现类为 FlushRealTimeService。看到这个默认间隔为 500ms,大家是不是会猜测 FlushRealTimeService 是使用了定时任务?
其实不然。这里引入了带超时时间的 CountDown await 方法,这样做的好处时如果没有新的消息写入,会休眠 500ms,但收到了新的消息后,可以被唤醒,做到消息及时被刷盘,而不是一定要等 500 ms。
我们首先来看一下 RocketMQ 的文件转发机制:
在 RocketMQ 中数据会首先写入到 commitlog 文件,而 consumequeue、indexFile 等文件都是基于 commitlog 文件异步进行转发的,既然是异步的,就有可能出现 commitlog 文件、consumequeue 文件中的数据不一致,例如在关闭 RocketMQ 中部分数据并没有转发给 consumequeue,那在重启时如何恢复,确保数据一致呢?
在讲解 RocketMQ 文件恢复机制之前,先抛出几个异常场景:
温馨提示:各位读者朋友们,建议大家在这里稍微停留片刻,对上述问题进行一个简单的思考,再继续本文的后续内容。
在 RocketMQ 中 Broker 异常停止恢复和正常停止恢复两种场景。
这两种场景主要是定位到从哪个文件开始恢复的逻辑不一样,一旦定位到从哪个文件,文件的恢复思路如下:
在实际生产环境下,如何高效的定位大概率需要恢复的文件呢?例如现在 commitlog 文件有 500 多个文件,从第一个文件开始判断?当然不是。会按照是否是正常退出还是异常退出。
在 RocketMQ 启动时候会创建一个名为 abort 的文件,然后在正常退出时会删除该文件,故判断 RocketMQ 进程是否是异常退出只需要查看 abort 文件是否存在,如果存在表示异常退出。
正常退出文件的定位策略:
RocketMQ 正常退出时可以从倒数第三个文件开始恢复,这个看似存在风险,但其实不然,因为通常情况一个文件写完,就会被刷写到磁盘中,但异常退出时就不能就不知道是什么原因退出的,这个时候就不能这么“随意”,必须严谨,那如何在严谨的情况下提高定位的效率呢?
不知大家有留意到上图中的 checkpoint 文件,相信大家对这个文件的含义不会陌生,在 RocketMQ 中会存储 commitlog、index、consumequeue 等文件的最后一次刷盘时间戳。其文件结构如下图所示:
该文件的刷盘机制如下:
从这里可以看出,commitlog 刷盘成功后,才会执行 checkpoint 文件的刷盘,commitlog 文件与 checkpoint 会存在不一致的情况,即 checkpoint 中存储的刷盘点以前的数据一定被写入到磁盘中,但并不能说只有 checkpoint 中的存储的刷盘点代表的数据并不能表示已刷盘的所有数据。
基于 checkpoint 文件的特点,异常退出时定位文件恢复的策略如下:
文件恢复的具体代码我这里就不做过多阐述了,根据上面的设计理念,自己顺藤摸瓜,效果应该会事半功倍。文件恢复的入口:DefaultMessageStore#recover。
在面向文件文件、面向网络的编程模型中,“零拷贝”这个词出现的频率我想是非常高的,在这里我并不打算普及 Java 零拷贝的具体含义,如果对其不太了解,建议百度之,这里我们看一下 RocketMQ 在消息消费时是如何基于 Netty 使用零拷贝的。
零拷贝的关键实现要点:
ManyMessageTransfer 的 transforeTo 方法实现如下图所示:
本文基于文件编程的模型就介绍到这里了,本文的设计思路是向优秀的人学习优秀的编程技巧。